'use strict';

const _ = require('lodash');
const { logger } = require('./utils/logger');
const debug = logger.debugContext('hooks');

const hookTypes = {
  beforeValidate: { params: 2 },
  afterValidate: { params: 2 },
  validationFailed: { params: 3 },
  beforeCreate: { params: 2 },
  afterCreate: { params: 2 },
  beforeDestroy: { params: 2 },
  afterDestroy: { params: 2 },
  beforeRestore: { params: 2 },
  afterRestore: { params: 2 },
  beforeUpdate: { params: 2 },
  afterUpdate: { params: 2 },
  beforeSave: { params: 2, proxies: ['beforeUpdate', 'beforeCreate'] },
  afterSave: { params: 2, proxies: ['afterUpdate', 'afterCreate'] },
  beforeUpsert: { params: 2 },
  afterUpsert: { params: 2 },
  beforeBulkCreate: { params: 2 },
  afterBulkCreate: { params: 2 },
  beforeBulkDestroy: { params: 1 },
  afterBulkDestroy: { params: 1 },
  beforeBulkRestore: { params: 1 },
  afterBulkRestore: { params: 1 },
  beforeBulkUpdate: { params: 1 },
  afterBulkUpdate: { params: 1 },
  beforeFind: { params: 1 },
  beforeFindAfterExpandIncludeAll: { params: 1 },
  beforeFindAfterOptions: { params: 1 },
  afterFind: { params: 2 },
  beforeCount: { params: 1 },
  beforeDefine: { params: 2, sync: true, noModel: true },
  afterDefine: { params: 1, sync: true, noModel: true },
  beforeInit: { params: 2, sync: true, noModel: true },
  afterInit: { params: 1, sync: true, noModel: true },
  beforeAssociate: { params: 2, sync: true },
  afterAssociate: { params: 2, sync: true },
  beforeConnect: { params: 1, noModel: true },
  afterConnect: { params: 2, noModel: true },
  beforeDisconnect: { params: 1, noModel: true },
  afterDisconnect: { params: 1, noModel: true },
  beforeSync: { params: 1 },
  afterSync: { params: 1 },
  beforeBulkSync: { params: 1 },
  afterBulkSync: { params: 1 },
  beforeQuery: { params: 2 },
  afterQuery: { params: 2 }
};
exports.hooks = hookTypes;


/**
 * get array of current hook and its proxies combined
 *
 * @param {string} hookType any hook type @see {@link hookTypes}
 *
 * @private
 */
const getProxiedHooks = hookType =>
  hookTypes[hookType].proxies
    ? hookTypes[hookType].proxies.concat(hookType)
    : [hookType]
;

function getHooks(hooked, hookType) {
  return (hooked.options.hooks || {})[hookType] || [];
}

const Hooks = {
  /**
   * Process user supplied hooks definition
   *
   * @param {object} hooks hooks definition
   *
   * @private
   * @memberof Sequelize
   * @memberof Sequelize.Model
   */
  _setupHooks(hooks) {
    this.options.hooks = {};
    _.map(hooks || {}, (hooksArray, hookName) => {
      if (!Array.isArray(hooksArray)) hooksArray = [hooksArray];
      hooksArray.forEach(hookFn => this.addHook(hookName, hookFn));
    });
  },

  async runHooks(hooks, ...hookArgs) {
    if (!hooks) throw new Error('runHooks requires at least 1 argument');

    let hookType;

    if (typeof hooks === 'string') {
      hookType = hooks;
      hooks = getHooks(this, hookType);

      if (this.sequelize) {
        hooks = hooks.concat(getHooks(this.sequelize, hookType));
      }
    }

    if (!Array.isArray(hooks)) {
      hooks = [hooks];
    }

    // synchronous hooks
    if (hookTypes[hookType] && hookTypes[hookType].sync) {
      for (let hook of hooks) {
        if (typeof hook === 'object') {
          hook = hook.fn;
        }

        debug(`running hook(sync) ${hookType}`);
        hook.apply(this, hookArgs);
      }
      return;
    }

    // asynchronous hooks (default)
    for (let hook of hooks) {
      if (typeof hook === 'object') {
        hook = hook.fn;
      }

      debug(`running hook ${hookType}`);
      await hook.apply(this, hookArgs);
    }
  },

  /**
   * Add a hook to the model
   *
   * @param {string}          hookType hook name @see {@link hookTypes}
   * @param {string|Function} [name] Provide a name for the hook function. It can be used to remove the hook later or to order hooks based on some sort of priority system in the future.
   * @param {Function}        fn The hook function
   *
   * @memberof Sequelize
   * @memberof Sequelize.Model
   */
  addHook(hookType, name, fn) {
    if (typeof name === 'function') {
      fn = name;
      name = null;
    }

    debug(`adding hook ${hookType}`);
    // check for proxies, add them too
    hookType = getProxiedHooks(hookType);

    hookType.forEach(type => {
      const hooks = getHooks(this, type);
      hooks.push(name ? { name, fn } : fn);
      this.options.hooks[type] = hooks;
    });

    return this;
  },

  /**
   * Remove hook from the model
   *
   * @param {string} hookType @see {@link hookTypes}
   * @param {string|Function} name name of hook or function reference which was attached
   *
   * @memberof Sequelize
   * @memberof Sequelize.Model
   */
  removeHook(hookType, name) {
    const isReference = typeof name === 'function' ? true : false;

    if (!this.hasHook(hookType)) {
      return this;
    }

    debug(`removing hook ${hookType}`);

    // check for proxies, add them too
    hookType = getProxiedHooks(hookType);

    for (const type of hookType) {
      this.options.hooks[type] = this.options.hooks[type].filter(hook => {
        if (isReference && typeof hook === 'function') {
          return hook !== name; // check if same method
        }
        if (!isReference && typeof hook === 'object') {
          return hook.name !== name;
        }
        return true;
      });
    }

    return this;
  },

  /**
   * Check whether the mode has any hooks of this type
   *
   * @param {string} hookType @see {@link hookTypes}
   *
   * @alias hasHooks
   *
   * @memberof Sequelize
   * @memberof Sequelize.Model
   */
  hasHook(hookType) {
    return this.options.hooks[hookType] && !!this.options.hooks[hookType].length;
  }
};
Hooks.hasHooks = Hooks.hasHook;


function applyTo(target, isModel = false) {
  _.mixin(target, Hooks);

  for (const hook of Object.keys(hookTypes)) {
    if (isModel && hookTypes[hook].noModel) {
      continue;
    }
    target[hook] = function(name, callback) {
      return this.addHook(hook, name, callback);
    };
  }
}
exports.applyTo = applyTo;

/**
 * A hook that is run before validation
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name beforeValidate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after validation
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name afterValidate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run when validation fails
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options, error. Error is the
 * SequelizeValidationError. If the callback throws an error, it will replace the original validation error.
 * @name validationFailed
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before creating a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeCreate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after creating a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name afterCreate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate`
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeSave
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before upserting
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeUpsert
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after upserting
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with the result of upsert(), options
 * @name afterUpsert
 * @memberof Sequelize.Model
 */

/**
  * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate`
 *
  * @param {string}   name
  * @param {Function} fn   A callback function that is called with attributes, options
  * @name afterSave
  * @memberof Sequelize.Model
  */

/**
 * A hook that is run before destroying a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name beforeDestroy
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after destroying a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name afterDestroy
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before restoring a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name beforeRestore
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after restoring a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name afterRestore
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before updating a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name beforeUpdate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after updating a single instance
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name afterUpdate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before creating instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instances, options
 * @name beforeBulkCreate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after creating instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instances, options
 * @name afterBulkCreate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before destroying instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name beforeBulkDestroy
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after destroying instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name afterBulkDestroy
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before restoring instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name beforeBulkRestore
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after restoring instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name afterBulkRestore
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before updating instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeBulkUpdate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after updating instances in bulk
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name afterBulkUpdate
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFind
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFindAfterExpandIncludeAll
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query, after all option parsing is complete
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFindAfterOptions
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run after a find (select) query
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with instance(s), options
 * @name afterFind
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before a count query
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeCount
 * @memberof Sequelize.Model
 */

/**
 * A hook that is run before a define call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeDefine
 * @memberof Sequelize
 */

/**
 * A hook that is run after a define call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with factory
 * @name afterDefine
 * @memberof Sequelize
 */

/**
 * A hook that is run before Sequelize() call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with config, options
 * @name beforeInit
 * @memberof Sequelize
 */

/**
 * A hook that is run after Sequelize() call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with sequelize
 * @name afterInit
 * @memberof Sequelize
 */

/**
 * A hook that is run before a connection is created
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with config passed to connection
 * @name beforeConnect
 * @memberof Sequelize
 */

/**
 * A hook that is run after a connection is created
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with the connection object and the config passed to connection
 * @name afterConnect
 * @memberof Sequelize
 */

/**
 * A hook that is run before a connection is disconnected
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with the connection object
 * @name beforeDisconnect
 * @memberof Sequelize
 */

/**
 * A hook that is run after a connection is disconnected
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with the connection object
 * @name afterDisconnect
 * @memberof Sequelize
 */

/**
 * A hook that is run before Model.sync call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options passed to Model.sync
 * @name beforeSync
 * @memberof Sequelize
 */

/**
 * A hook that is run after Model.sync call
 *
 * @param {string}   name
 * @param {Function} fn   A callback function that is called with options passed to Model.sync
 * @name afterSync
 * @memberof Sequelize
 */

/**
  * A hook that is run before sequelize.sync call
 *
  * @param {string}   name
  * @param {Function} fn   A callback function that is called with options passed to sequelize.sync
  * @name beforeBulkSync
  * @memberof Sequelize
  */

/**
  * A hook that is run after sequelize.sync call
 *
  * @param {string}   name
  * @param {Function} fn   A callback function that is called with options passed to sequelize.sync
  * @name afterBulkSync
  * @memberof Sequelize
  */
